@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.
- package/README.md +94 -217
- package/dist/agents/copilot-cli.js +7 -2
- package/dist/agents/persistence.js +83 -8
- package/dist/agents/registry.js +5 -2
- package/dist/agents/routing.js +31 -14
- package/dist/api/analyze.js +138 -41
- package/dist/audit/deep.js +2 -1
- package/dist/audit/detectors.js +23 -2
- package/dist/audit/rules.js +4 -1
- package/dist/audit-ui/export.js +5 -4
- package/dist/auth/device-login.js +33 -11
- package/dist/auth/login-app.js +17 -12
- package/dist/auth/store.js +65 -9
- package/dist/bin/dg.js +1 -1
- package/dist/commands/audit.js +12 -20
- package/dist/commands/{guard-commit.js → commit-guard.js} +12 -12
- package/dist/commands/cooldown.js +2 -1
- package/dist/commands/decisions.js +8 -7
- package/dist/commands/help.js +11 -3
- package/dist/commands/licenses.js +17 -8
- package/dist/commands/login.js +58 -21
- package/dist/commands/router.js +12 -2
- package/dist/commands/sbom.js +20 -5
- package/dist/commands/scan.js +5 -4
- package/dist/commands/service.js +1 -1
- package/dist/commands/setup.js +12 -12
- package/dist/commands/status.js +4 -4
- package/dist/commands/uninstall.js +1 -1
- package/dist/commands/update.js +20 -0
- package/dist/commands/verify.js +4 -4
- package/dist/config/settings.js +81 -12
- package/dist/export-ui/ExportDialog.js +1 -1
- package/dist/gate/cooldown-request.js +22 -0
- package/dist/gate/egress-guard.js +185 -0
- package/dist/gate/host-match.js +48 -0
- package/dist/gate/verdict-core.js +101 -0
- package/dist/gate/verdict-fetch.js +214 -0
- package/dist/launcher/agent-check.js +1096 -42
- package/dist/launcher/agent-hook-exec.js +8 -1
- package/dist/launcher/agent-hook-io.js +5 -3
- package/dist/launcher/classify.js +110 -26
- package/dist/launcher/env.js +84 -18
- package/dist/launcher/install-preflight.js +26 -3
- package/dist/launcher/live-install.js +4 -2
- package/dist/launcher/manifest-screen.js +88 -7
- package/dist/launcher/output-redaction.js +153 -15
- package/dist/launcher/preflight-prompt.js +1 -1
- package/dist/launcher/resolve-real-binary.js +8 -3
- package/dist/launcher/run.js +237 -40
- package/dist/origin/artifact-cache.js +55 -0
- package/dist/origin/cargo-config.js +12 -0
- package/dist/origin/gate-mode.js +7 -0
- package/dist/origin/platform-paths.js +14 -0
- package/dist/origin/precedence.js +345 -0
- package/dist/origin/server.js +611 -0
- package/dist/origin/spike-worker.js +35 -0
- package/dist/origin/worker.js +116 -0
- package/dist/presentation/package-page.js +21 -1
- package/dist/presentation/safe-version.js +11 -0
- package/dist/presentation/sarif.js +109 -0
- package/dist/project/dgfile.js +54 -17
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/enforcement.js +27 -7
- package/dist/proxy/metadata-map.js +33 -7
- package/dist/proxy/preverified.js +0 -3
- package/dist/proxy/server.js +104 -446
- package/dist/proxy/upstream-proxy.js +23 -4
- package/dist/proxy/worker.js +3 -2
- package/dist/publish-set/collect.js +6 -0
- package/dist/runtime/fatal.js +2 -1
- package/dist/runtime/first-run.js +5 -1
- package/dist/runtime/node-version.js +51 -0
- package/dist/runtime/nudges.js +27 -1
- package/dist/sbom/cyclonedx.js +120 -33
- package/dist/sbom/graph.js +236 -0
- package/dist/sbom/spdx.js +53 -0
- package/dist/sbom-ui/SbomApp.js +5 -3
- package/dist/sbom-ui/inventory.js +8 -10
- package/dist/scan/analyze-worker.js +3 -1
- package/dist/scan/collect.js +38 -8
- package/dist/scan/command.js +77 -27
- package/dist/scan/discovery.js +18 -7
- package/dist/scan/installed.js +202 -0
- package/dist/scan/manifest-coverage.js +128 -0
- package/dist/scan/render.js +125 -40
- package/dist/scan/scanner-report.js +92 -22
- package/dist/scan/staged.js +68 -15
- package/dist/scan/types.js +7 -1
- package/dist/scan-ui/LegacyApp.js +3 -3
- package/dist/scan-ui/components/InteractiveResultsView.js +17 -14
- package/dist/scan-ui/hooks/useScan.js +37 -11
- package/dist/scan-ui/launch.js +2 -2
- package/dist/scan-ui/logo.js +6 -2
- package/dist/scan-ui/shims.js +6 -6
- package/dist/scripts/gate.js +53 -35
- package/dist/security/csv.js +9 -0
- package/dist/security/sanitize.js +11 -2
- package/dist/service/state.js +120 -37
- package/dist/service/trust-refresh.js +12 -1
- package/dist/service/trust-store.js +36 -5
- package/dist/service/worker.js +5 -7
- package/dist/setup/git-hook.js +2 -2
- package/dist/setup/install-method.js +36 -0
- package/dist/setup/plan.js +119 -57
- package/dist/setup/uninstall-standalone.js +7 -2
- package/dist/setup-ui/gate.js +1 -8
- package/dist/setup-ui/offer.js +14 -7
- package/dist/setup-ui/selector.js +1 -1
- package/dist/setup-ui/wizard.js +73 -78
- package/dist/standalone/uninstall.mjs +1114 -386
- package/dist/state/cleanup-registry.js +14 -3
- package/dist/state/cooldown-held.js +36 -13
- package/dist/state/locks.js +239 -34
- package/dist/state/sessions.js +2 -2
- package/dist/util/sh-escape.js +6 -0
- package/dist/util/tty-prompt.js +13 -4
- package/dist/verify/local.js +60 -10
- package/dist/verify/package-check.js +35 -49
- package/dist/verify/preflight.js +59 -33
- package/dist/verify/render.js +11 -12
- package/npm-shrinkwrap.json +714 -213
- 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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
30
|
-
|
|
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
|
|
34
|
+
dg login # free account
|
|
35
|
+
dg scan # audit the current project's lockfiles
|
|
40
36
|
```
|
|
41
37
|
|
|
42
|
-
`dg scan` reads your lockfiles
|
|
43
|
-
dependency
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
pip
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 <registry:package[@version] | path | lockfile>
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
92
|
+
### dg sbom
|
|
147
93
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
98
|
+
### dg audit
|
|
155
99
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
110
|
+
### dg decisions
|
|
173
111
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
116
|
+
### dg cooldown
|
|
181
117
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
122
|
+
## In CI
|
|
190
123
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
130
|
+
## Privacy
|
|
198
131
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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).
|
|
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:
|
|
87
|
-
|
|
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 {
|
|
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
|
|
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,
|
|
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 {
|
package/dist/agents/registry.js
CHANGED
|
@@ -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
|
-
|
|
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;
|